package com.spotalog.server.service;

import static com.googlecode.objectify.ObjectifyService.ofy;

import java.util.Date;
import java.util.List;
import java.util.Map;

import javax.annotation.Nonnegative;
import javax.annotation.Nullable;
import javax.inject.Inject;

import lombok.extern.slf4j.Slf4j;

import com.google.appengine.api.blobstore.BlobKey;
import com.google.appengine.api.blobstore.BlobstoreService;
import com.google.appengine.api.search.Cursor;
import com.google.appengine.api.search.Query;
import com.google.appengine.api.search.QueryOptions;
import com.google.appengine.api.search.Results;
import com.google.appengine.api.search.ScoredDocument;
import com.google.appengine.api.search.SearchException;
import com.google.appengine.api.search.SortExpression;
import com.google.appengine.api.search.SortOptions;
import com.google.common.base.Optional;
import com.google.common.base.Predicates;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.TxnType;
import com.spotalog.server.annotations.Transact;
import com.spotalog.server.service.search.Queries;
import com.spotalog.shared.annotations.Limit;
import com.spotalog.shared.model.Address;
import com.spotalog.shared.model.Photo;
import com.spotalog.shared.model.Radius;
import com.spotalog.shared.model.Spot;
import com.spotalog.shared.model.SpotType;
import com.spotalog.shared.service.rest.Envelope;

@Slf4j
public class SpotService extends AbstractService<Spot> {
	private final int defaultLimit;
	private final BlobstoreService blobstoreService;

	@Inject
	SpotService(BlobstoreService blobstoreService, @Limit int defaultLimit) {
		super(Spot.class);
		this.defaultLimit = defaultLimit;
		this.blobstoreService = blobstoreService;
	}

	public List<Spot> query(@Nullable Radius radius,
			@Nullable List<String> keywords, @Nullable SpotType type,
			@Nullable Date altered, @Nullable Date created,
			@Nullable String cursorString, @Nonnegative int limit) {
		return queryWithCursor(
				radius,
				keywords,
				type,
				altered,
				created,
				Strings.isNullOrEmpty(cursorString) ? null
						: buildCursor(cursorString), limit).getResults();
	}

	public Envelope<Spot> queryWithCursor(@Nullable Radius radius,
			@Nullable List<String> keywords, @Nullable SpotType type,
			@Nullable Date altered, @Nullable Date created,
			@Nullable Cursor cursor, @Nonnegative int limit) {
		List<String> queryString = Lists.newArrayList();

		if (radius != null) {
			queryString.add(Queries.distance(Spot.LOCATION, radius.getCenter(),
					radius.getRadius()));
		}

		if (keywords != null) {
			List<String> or = Lists.newArrayList();
			for (String keyword : keywords) {
				or.add(Queries.keywords(keyword, Spot.DESCRIPTION, Spot.NAME,
						Address.ADDRESS, Address.CITY, Address.COUNTRY,
						Address.STATE, Address.ZIPCODE));
			}
			queryString.add(Queries.or(or));
		}

		if (type != null) {
			queryString.add(Queries.type(Spot.TYPE, type));
		}

		Results<ScoredDocument> results = null;
		try {
			int queryLimit = limit == 0 ? this.defaultLimit : limit;
			SortOptions.Builder sortOptionsBuilder = this.sortOptions.get()
					.setLimit(queryLimit);

			if (radius != null) {
				sortOptionsBuilder.addSortExpression(this.sortExpressions
						.get()
						.setExpression(
								Queries.distance(Spot.LOCATION,
										radius.getCenter()))
						.setDefaultValueNumeric(Double.MAX_VALUE)
						.setDirection(SortExpression.SortDirection.DESCENDING));
			}

			QueryOptions.Builder options = this.queryOptions.get()
					.setLimit(queryLimit).setFieldsToReturn(Spot.ID)
					.setSortOptions(sortOptionsBuilder.build());
			if (cursor != null) {
				options.setCursor(cursor);
			}

			Query query = this.queryBuilers.get().setOptions(options)
					.build(Queries.and(queryString));
			results = getIndex().search(query);
		} catch (SearchException e) {
			log.error("Search request with query [{}] failed: {}", queryString,
					e);
		}

		List<Long> ids = Lists.newArrayList();
		for (ScoredDocument document : results) {
			ids.add(getKey(document.getId()));
		}

		Cursor resultCursor = results.getCursor();

		Map<Long, Spot> fetched = ofy().load().type(Spot.class).ids(ids);

		return new Envelope<Spot>(fetched.values(), fetched.size(),
				resultCursor == null ? null : resultCursor.toWebSafeString());
	}

	@Override
	@Transact(TxnType.REQUIRED)
	public void delete(long id) {
		Optional<Spot> read = read(id);
		if (read.isPresent()) {
			Spot spot = read.get();
			if (spot.getPhotos() != null) {
				List<BlobKey> keys = Lists.newArrayList();
				for (Photo photo : Iterables.filter(spot.getPhotos(),
						Predicates.notNull())) {
					try {
						keys.add(new BlobKey(photo.getBlobKey()));
					} catch (Exception ex) {
						log.error("{}", ex);
					}
				}
				try {
					this.blobstoreService.delete(Iterables.toArray(keys,
							BlobKey.class));
				} catch (Exception ex) {
					log.error("{}", ex);
				}
			}
		}
		ofy().delete().key(Key.create(Spot.class, id));
		removeFromIndex(id);
	}
}
